﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Reflection;
using System.Collections;
using System.ComponentModel;

using Idlesoft.Utils;

namespace Idlesoft.Cli
{
	using Generators = Dictionary<FlagTypes, Func<string, IEnumerable<string>>>;

	[Flags]
	public enum FlagTypes : int
	{
		Undef		= 0,
		Posix		= 1,
		Dos			= 2,
		Gnu			= 4,
		Java		= 8,
		Microsoft	= 16,
		Default		= Gnu | Posix,
		All			= Dos | Gnu | Java | Posix | Microsoft
	}

	public enum ValueTypes : int
	{
		Undef,
		Boolean,
		Single,
		Multiple
	}

	public static class CliBuilder
	{
		#region consts
		private const int MIN_WIDTH = 15;
		private const string SLASH = "/";
		private const string DASH = "-";
		private const string DASHX2 = "--";
		private const string SPACE = " ";
		private const string EQUALS = "=";
		private const string TAB = "    ";
		private const string PIPE = "|";
		#endregion consts

		#region ctor
		static CliBuilder()
		{
			ExitOnError = true;
			LoadDefaultGenerators();
			Usage = DefaultUsage;
		}
		#endregion ctor

		#region instantiators
		/// <summary>
		/// The main Cli method for instantiating your class object that has been decorated with CliAttributes or CliElements.  The args are parsed and assigned to appropriately decorated properties or fields.
		/// </summary>
		/// <typeparam name="TTarget">The type of class to be instantiated.  Should have properties|fields decorated with Cli attributes.</typeparam>
		/// <param name="title">Optional name to be used in the help usage print screen.  Will default to name of executing assembly.</param>
		/// <param name="description">Optional summary to be used in the hep usage print screen.  Will default to empty string.</param>
		/// <param name="flags">Optional override for the flag types for all properties and fields to use if not specified.  Defaults FlagTypes.Default</param>
		/// <param name="args">Optional array of arguments to be parsed.  Defaults to Environment.GetCommandLineArgs()</param>
		/// <returns>Returns instantiated class with properties and fields assigned with parsed values.</returns>
		public static TTarget Create<TTarget>(string title = null, string description = null, FlagTypes flags = FlagTypes.Default, params string[] args)
			where TTarget : class
		{
			return Create<TTarget>(null, title, description, flags, args);
		}

		/// <summary>
		/// The main Cli method for instantiating your class object that has been decorated with CliAttributes or CliElements.  The args are parsed and assigned to appropriately decorated properties or fields.
		/// </summary>
		/// <typeparam name="TTarget">The type of class to be instantiated.  Should have properties|fields decorated with Cli attributes.</typeparam>
		/// <param name="params">Optional object array of values to be used in Activator.Instantiate(typeof(TTarget), params)</param>
		/// <param name="title">Optional name to be used in the help usage print screen.  Will default to name of executing assembly.</param>
		/// <param name="description">Optional summary to be used in the hep usage print screen.  Will default to empty string.</param>
		/// <param name="flags">Optional override for the flag types for all properties and fields to use if not specified.  Defaults FlagTypes.Default</param>
		/// <param name="args">Optional array of arguments to be parsed.  Defaults to Environment.GetCommandLineArgs()</param>
		/// <returns>Returns instantiated class with properties and fields assigned with parsed values.</returns>
		public static TTarget Create<TTarget>(object[] @params, string title = null, string description = null, FlagTypes flags = FlagTypes.Default, params string[] args)
			where TTarget : class
		{
			TTarget target = default(TTarget);
			_Flags = new Dictionary<string, int>();
			Title = title ?? Filename + " Program";
			Description = description ?? "";
			Flags = flags != FlagTypes.Undef ? flags : FlagTypes.Default;
			Args = !args.IsNullOrEmpty() ? args : Environment.GetCommandLineArgs().Skip(1).ToArray();

			try
			{
				Load<TTarget>();
				target = Instantiate<TTarget>(@params);
				target = Assign<TTarget>(target);
			}
			catch (Exception ex)
			{
				ex.Print();
				Usage(Title, Description, _CliAttributes).Print();
				if (ExitOnError)
					Environment.Exit(1);
				else
					throw ex;
			}
			return target;
		}

		private static TTarget Instantiate<TTarget>(object[] @params)
			where TTarget : class
		{
			TTarget target = default(TTarget);

			target = LoadConfigFiles<TTarget>(LoadConfigs);

			if (target == null)
			{
				if (!@params.IsNullOrEmpty())
					target = Activator.CreateInstance(typeof(TTarget), @params) as TTarget;
				else
					target = Activator.CreateInstance(typeof(TTarget)) as TTarget;
			}

			return target;
		}
		private static void Load<TTarget>()
			where TTarget : class
		{
			var members = GetCliMembers(typeof(TTarget)).Plus(GetCliMembers(typeof(CliBuilder)));
			_CliAttributes = GetCliAttributes(members);
			LoadValues(_CliAttributes);
			RemainingArgs = Args.Where(arg => !_CliAttributes.Any(att => att.MatchesArg(arg) ) ).ToArray();
			var builtins = _CliAttributes.Where(cli => cli.BuiltIn).ToArray();
			EvalCliAttributes(null, builtins);

			if (Help)
			{
				Usage(Title, Description, _CliAttributes).Print();
				Environment.Exit(0);
			}
		}

		private static TTarget Assign<TTarget>(TTarget target)
		{
			var options = _CliAttributes.Where(cli => !cli.BuiltIn).ToArray();
			EvalCliAttributes(target, options);

			if (!SaveConfig.IsNullOrEmpty())
				SaveConfigFile<TTarget>(target, SaveConfig);
			if (SaveConfig != null)
				SaveConfigFile<TTarget>(target, XmlFilename);

			return target;
		}
		#endregion instantiators

		#region props
		public static bool ExitOnError { get; set; }

		/// <summary>
		/// Produces the name of current process' main module filename
		/// </summary>
		public static string Filename { get { return Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName); } }

		/// <summary>
		/// Produces the Filename with the .xml extension instead
		/// </summary>
		public static string XmlFilename { get { return Path.ChangeExtension(Filename, ".xml"); } }

		/// <summary>
		/// Produces the Filename with the .ini extension instead
		/// </summary>
		public static string IniFilename { get { return Path.ChangeExtension(Filename, ".ini"); } }

		/// <summary>
		/// Produces the Filename with the .cli extension instead
		/// </summary>
		public static string CliFilename { get { return Path.ChangeExtension(Filename, ".cli"); } }

		/// <summary>
		/// The title to be used in help usage printing
		/// </summary>
		public static string Title { get; set; }

		/// <summary>
		/// The decsription to be used in help usage printing
		/// </summary>
		public static string Description { get; set; }

		/// <summary>
		/// The default flags value to be used on CliAttributes that don't otherwise specify
		/// </summary>
		public static FlagTypes Flags { get; internal set; }

		/// <summary>
		/// The args to be used in parsing, either passed in with the Create method or assumed to be Environment.GetCommandLineArgs()
		/// </summary>
		public static string[] Args { get; internal set; }

		/// <summary>
		/// Is set to the remaining args that were not found in the class when instantiating an object via CliBuilder.Create
		/// </summary>
		public static string[] RemainingArgs { get; internal set; }

		/// <summary>
		/// Dictionary of flag values and expression generators to be used for automatically divining flag tokens to match against in parsing
		/// </summary>
		public static Generators Generators { get; internal set; }

		/// <summary>
		/// Method to be used when 'help' is invoked; can be overriden by replacing this function with your own printing function
		/// </summary>
		public static Func<string, string, ICliAttribute[], string> Usage { get; set; }

		/// <summary>
		/// Built-In property to match against Help and print the usage text.  Will default to the common -h|--help unless other flags specified override this behavior
		/// </summary>
		[CliAttribute(Description = "show this help message and exit")]
		public static bool Help { get; set; }

		/// <summary>
		/// Built-In property to match against Version and print the version text.  Will default to the common -v|--version unless other flags specified override this behavior
		/// </summary>
		[CliAttribute(Description = "return the version of the program")]
		public static bool Version { get; set; }

		/// <summary>
		/// Built-In property to match against CliVersion and print the version of cli text.  Will default to the common -c|--cliversion unless other flags specified override this behavior
		/// </summary>
		[CliAttribute(Description = "return the version of the Cli assembly")]
		public static bool CliVersion { get; set; }

		/// <summary>
		/// Built-In property to match against SaveSonfig which is a path, filename and extension that specifies where, with what name and what type of config file to save from the parameters that are loaded during Create
		/// </summary>
		[CliAttribute(Description = "save the parameters as an xml file")]
		public static string SaveConfig { get; set; }

		/// <summary>
		/// Built-In property to match against LoadConfigs which is an array of paths to configs files (.xml, .ini, .cli) that will load values PRIOR to the other commandline options that may be included
		/// </summary>
		[CliElement(Description = "list of cli config files to load")]
		public static string[] LoadConfigs { get; set; }
		#endregion props

		#region methods

		#region load
		private static MemberInfo[] GetCliMembers(Type type)
		{
			return type.GetMembers().Where(mi => mi.GetCustomAttributes(true).Any(cli => cli.GetType().IsType(typeof(ICliAttribute)))).ToArray();
		}

		private static ICliAttribute[] GetCliAttributes(MemberInfo[] members)
		{
			return members.Select(mi => FinalizeCliAttribute(mi)).OrderByDescending(cli => !cli.Flags.IsNullOrEmpty()).Select(cli => GenerateFlags(cli)).ToArray();
		}

		private static ICliAttribute FinalizeCliAttribute(MemberInfo mi)
		{
			var cliattribute = mi.GetCustomAttributes(true).First(att => att.GetType().IsType(typeof(ICliAttribute))) as ICliAttribute;
			cliattribute.MemberInfo = mi;
			if (cliattribute.Name == null)
				cliattribute.Name = mi.Name;
			if (cliattribute.FlagTypes == FlagTypes.Undef)
				cliattribute.FlagTypes = Flags;
			return cliattribute;
		}

		private static ICliAttribute GenerateFlags(ICliAttribute cliattribute)
		{
			if (cliattribute.Flags.IsNullOrEmpty())
			{
				List<string> flags = new List<string>();
				foreach (var flag in cliattribute.FlagTypes.GetFlags())
				{
					if (Generators.ContainsKey((FlagTypes)flag))
					{
						var generator = Generators[(FlagTypes)flag];
						foreach (var possFlag in generator(cliattribute.Name))
						{
							if (!_Flags.ContainsKey(possFlag))
							{
								_Flags[possFlag] = 1;
								flags.Add(possFlag);
								break;
							}
						}
					}
				}
				cliattribute.Flags = flags.ToArray();
			}
			else
			{
				foreach (var flag in cliattribute.Flags)
				{
					if (_Flags.ContainsKey(flag))
						throw new ArgumentException(string.Format("cannot have identical flags: {0}", flag) );
					_Flags[flag] = 1;
				}
			}
			return cliattribute;
		}

		private static void LoadValues(ICliAttribute[] cliattributes)
		{
			//return cliattributes.Select(cli => MatchArgs(cli) ).ToArray();
			cliattributes.ForEach(cli => MatchArgs(cli));
			//return cliattributes;
		}

		private static ICliAttribute MatchArgs(ICliAttribute cliattribute)
		{
			foreach (var flag in cliattribute.Flags)
			{
				var values = Args.SkipWhile(arg => arg != flag).TakeWhile(arg => !_Flags.Keys.Any(flg => flag != flg && flg == arg)).ToArray();
				if (values.Length > 0)
				{
					switch (cliattribute.ValueTypes)
					{
						case ValueTypes.Undef:
							throw new ArgumentException("ValuesType.Undef in MatchArgs; unknown behavior");
						case ValueTypes.Boolean:
							cliattribute.Args = new object[] { values.First(), true };
							break;
						case ValueTypes.Single:
							if (values.Length < 2)
								throw new CliArgumentException(string.Format("{0} requires one value for flag {1}", ValueTypes.Single.EnumToString(), values.First() ) );
							cliattribute.Args = values.Take(2).ToArray();
							break;
						case ValueTypes.Multiple:
							cliattribute.Args = values;
							break;
						default:
							throw new ArgumentException("Unknown case in MatchArgs");
					}
				}
			}
			return cliattribute;
		}

		private static string DefaultUsage(string title, string description, ICliAttribute[] cliattributes)
		{
			var usage = string.Format("\n{0}\n\n{1}\n\nUsage:\n\n", title, description);
			var flagWidth = cliattributes.Max(cli => cli.Flags.Flatten(PIPE).Length) + 1;
			var typeWidth = cliattributes.Max(cli => cli.MemberInfoType.Pretty().Length) + 1;

			usage += PrintUsage("TYPE", typeWidth, "FLAGS", flagWidth, "DESCRIPTION");

			var reqs = cliattributes.Where(cli => cli.Required);
			if (!reqs.IsNullOrEmpty())
			{
				usage += "\nRequired Arguments:\n\n";
				foreach (var req in reqs)
					usage += PrintUsage(req, flagWidth, typeWidth);
			}

			var opts = cliattributes.Where(cli => cli.Option);
			if (!opts.IsNullOrEmpty())
			{
				usage += "\nOptional Arguments:\n\n";
				foreach (var opt in opts)
					usage += PrintUsage(opt, flagWidth, typeWidth);
			}

			var bis = cliattributes.Where(cli => cli.BuiltIn);
			if (!bis.IsNullOrEmpty())
			{
				usage += "\nBuilt-In Arguments:\n\n";
				foreach (var bi in bis)
					usage += PrintUsage(bi, flagWidth, typeWidth);
			}
			return usage + "\n";
		}
		
		private static string PrintUsage(ICliAttribute cliattribute, int flagWidth, int typeWidth)
		{
			return PrintUsage(cliattribute.MemberInfoType.Pretty(), typeWidth, cliattribute.Flags.Flatten("|"), flagWidth, cliattribute.Description);
		}

		private static string PrintUsage(string column1, int width1, string column2, int width2, string column3)
		{
			return TAB + column1.PadRight(width1) + column2.PadRight(width2) + column3 + "\n";
		}

		#region generators

		/// <summary>
		/// Default method to load default flag generators.
		/// </summary>
		public static void LoadDefaultGenerators()
		{
			Generators = new Generators();
			Generators[FlagTypes.Dos] = DosGenerator;
			Generators[FlagTypes.Gnu] = GnuGenerator;
			Generators[FlagTypes.Java] = JavaGenerator;
			Generators[FlagTypes.Posix] = PosixGenerator;
			Generators[FlagTypes.Microsoft] = MicrosoftGenerator;
		}

		private static IEnumerable<string> DosGenerator(string name)
		{
			foreach (var letter in MostSigChars(name))
			{
				for (var x = 0; x < 2; ++x)
				{
					char poss = '\0';
					switch (x)
					{
						case 0: poss = Char.ToUpper(letter); break;
						case 1: poss = Char.ToLower(letter); break;
					}
					yield return SLASH + poss;
				}
			}
		}

		private static IEnumerable<string> GnuGenerator(string name)
		{
			for (var x = 0; ; ++x)
			{
				for (var y = 0; y < 3; ++y)
				{
					string poss = "";
					switch (y)
					{
						case 0: poss = name.ToLower(); break;
						case 1: poss = name; break;
						case 2: poss = name.ToUpper(); break;
					}
					if (x > 0)
						poss += x;
					yield return DASHX2 + poss;
				}
			}
		}

		private static IEnumerable<string> JavaGenerator(string name)
		{
			for (var x = 0; ; ++x)
			{
				string poss = name;
				if (x > 0)
					poss += x;
				yield return DASH + poss + EQUALS;
			}
		}

		private static IEnumerable<string> PosixGenerator(string name)
		{
			foreach (var letter in MostSigChars(name))
			{
				for (var x = 0; x < 2; ++x)
				{
					char poss = '\0';
					switch (x)
					{
						case 0: poss = Char.ToLower(letter); break;
						case 1: poss = Char.ToUpper(letter); break;
					}
					yield return DASH + poss;
				}
			}
		}

		private static IEnumerable<string> MicrosoftGenerator(string name)
		{
			for (var x = 0; ; ++x)
			{
				string poss = name;
				if (x > 0)
					poss += x;
				yield return poss + EQUALS;
			}
		}

		private static char[] MostSigChars(string text)
		{
			List<char> sig = new List<char>();
			sig.Add(text.First());
			sig.AddRange(text.ToUpper());
			sig.AddRange(text.ToLower());
			sig.AddRange(Common.ATOZ);
			sig.AddRange(Common.atoz);
			return sig.Distinct().ToArray();
		}

		#endregion generators

		#endregion load

		#region instantiate

		private static TTarget LoadConfigFiles<TTarget>(string[] configs)
		{
			TTarget cli = default(TTarget);

			if (configs.IsNullOrEmpty())
				configs = XmlFilename.MakeArray();

			foreach (var config in configs)
			{
				if (config.FileExists())
				{
					try
					{
						cli = config.Deserialize<TTarget>();
						if (cli != null)
							break;
					}
					catch (Exception ex)
					{
						ex.Print();
					}
				}
			}

			return cli;
		}
		#endregion instantiate

		#region assign
		private static void EvalCliAttributes(object target, ICliAttribute[] cliattributes)
		{
			foreach (var cliattribute in cliattributes.OrderByDescending(cli => cli.Required))
			{
				EvalCliAttribute(target, cliattribute);
				if (cliattribute.Required && !cliattribute.IsAssigned(target))
					throw new ArgumentException("missing required flag");
			}
		}

		private static void EvalCliAttribute(object target, ICliAttribute cliattribute)
		{
			if (cliattribute.Values != null)
				Common.InvokeSetMember(cliattribute.MemberInfo, target, cliattribute.Values);
			else if (cliattribute.Default != null)
				Common.InvokeSetMember(cliattribute.MemberInfo, target, cliattribute.Default);
		}

		private static void SaveConfigFile<TTarget>(TTarget cli, string config)
		{
			cli.Serialize(config);
		}
		#endregion assign
		
		#endregion methods

		#region fields
		private static Dictionary<string, int> _Flags = null;
		private static ICliAttribute[] _CliAttributes = null;
		#endregion fields
	}
}

